/*
 * CastDRAndroid
 * Copyright (c) 2014 Christian Holm Christensen
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Definition of the Extractor class
 */
package org.cholm.media.castdr.backend;

import org.apache.http.client.utils.URIUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.cholm.media.castdr.LogUtils; import static org.cholm.media.castdr.LogUtils.LOGd;
import static org.cholm.media.castdr.LogUtils.LOGe;
import static org.cholm.media.castdr.LogUtils.LOGw;
// import org.apache.commons.logging.Log;
// import org.apache.commons.logging.LogFactory;
// import org.apache.log4j.BasicConfigurator;
// import org.apache.log4j.Level;
// import org.apache.log4j.LOGer;

public class Extractor {
    /**
     * Base URL of Mobile API
     */
    public static final String API_MOBIL_URL = "http://dr.dk/nu-mobil/api/";
    /** Base URL of API */
    // public static final String API_URL = "http://dr.dk/nu/api/";
    /**
     * Danish locale - used for string operations
     */
    public static final Locale DANISH = new Locale("da");
    /** Base URL of Meta data */
    // public static final String META_URL = "http://www.dr.dk/mu/programcard/";
    /** Width of images */
    // public static final int IMG_WIDTH = 120;
    /** Height of images */
    // public static final int IMG_HEIGHT = 90;
    /**
     * channel list
     */
    public static final String[] CHANNELS = {"DR1", "DR2", "DR3", "DR-K",
            "DR-Ultra", "DR-Ramasjang"};
    /**
     * Our tag
     */
    private static final String TAG = LogUtils.makeLogTag(Extractor.class);
    private static final int MAX_LETTER = 100;
    private static final int MAX_GENRE = 100;
    private static final int MAX_CHANNEL = 100;
    private static final int MAX_NEWEST = 30;
    /**
     * Instance
     */
    private static Extractor mInstance = null;
    /**
     * Rest client
     */
    private RestClient mRest = new RestClient();
    /**
     * Our map
     */
    private Map<String, Serie> mSeries = new HashMap<String, Serie>();
    /**
     * Map of live channels
     */
    private Category mLive = new Category("LIVE");
    /**
     * Map of genres
     */
    private Category mGenres = new Category("Genre");
    /**
     * Map of channels
     */
    private Category mChannels = new Category("Kanaler");
    /**
     * Map of genres
     */
    private Category mLetters = new Category("Alfabetisk");
    /**
     * Map of genres
     */
    private Category mNewest = new Category("Nyeste");
    /**
     * Map of search results
     */
    private Category mSearch = new Category("Søg");
    /**
     * List view of the categories
     */
    private List<Category> mList = new ArrayList<Category>();

    /**
     * Protected constructor
     */
    protected Extractor() {
    }

    public static Extractor instance() {
        if (mInstance == null) mInstance = new Extractor();
        return mInstance;
    }

    protected static String convChannel(String in) {
        if (in.equalsIgnoreCase("dr-ultra")) return "DR-Ultra";
        if (in.equalsIgnoreCase("dr-ramasjang")) return "DR-Ramasjang";
        return in.toUpperCase(DANISH);
    }

    protected void addToList(Category c, boolean sort) {
        LOGd(TAG, "Adding group " + c.getName());
        if (sort) c.sortIndex();
        mList.add(c);
    }

    /**
     * Get the list of categories
     *
     * @return List of categories
     */
    public List<Category> getList() {
        return mList;
    }

    /**
     * Setup our categories
     */
    public void setup() {
        this.setup(false);
    }

    /**
     * Setup our categories
     */
    public void setup(boolean force) {
        LOGd(TAG, "Setting up");
        if (!force && mList.size() > 0) {
            LOGd(TAG, "Already loaded, returning");
            return;
        }
        setupLive();
        setupNewest();
        setupGenres();
        setupChannels();
        setupLetters();
        setupSearch();
        addToList(getLIVE(), false);
        addToList(getNewest(), false);
        addToList(getChannels(), false);
        addToList(getGenres(), true);
        addToList(getLetters(), true);
        addToList(getSearch(), false);
    }

    /**
     * Set-up Channels category
     */
    protected void setupLive() {
        LOGd(TAG, "Set-up Live category");
        String url = "http://www.dr.dk/tv/external/channels?mediaType=tv";
        // String sprite = "http://www.dr.dk/assets/img/sprites/dr-logo-tv-xlarge-sprite.png";
        JSONObject json = null;
        if ((json = getJSONObject(url)) == null) return;

        Map<String, String> tmp = new HashMap<String, String>();
        JSONArray channels = json.optJSONArray("Data");
        for (int i = 0; i < channels.length(); i++) {
            JSONObject ch = channels.optJSONObject(i);
            if (ch == null || ch.optInt("ChannelType") != 0
                    || ch.optBoolean("WebChannel") == true) continue;

            String slug = ch.optString("Slug");
            JSONArray servers = ch.optJSONArray("StreamingServers");
            for (int isrv = 0; isrv < servers.length(); isrv++) {
                JSONObject srv = servers.optJSONObject(isrv);
                if (srv == null) continue;
                String type = srv.optString("LinkType");
                if (type == null || !type.equals("HLS")) continue;

                try {
                    String serv = srv.getString("Server");
                    JSONArray quals = srv.optJSONArray("Qualities");
                    JSONObject qual = quals.getJSONObject(0);
                    JSONArray strms = qual.getJSONArray("Streams");
                    JSONObject strm = strms.getJSONObject(0);
                    String path = strm.getString("Stream");
                    String h = serv + "/" + path + "&rebase=on";

                    tmp.put(slug, h);
                } catch (JSONException e) {
                    LOGw(TAG,
                            "Failed to get stream for " + slug + ": "
                                    + e.getMessage()
                    );
                }

            }
        }
        for (String c : CHANNELS) {
            String uc = c.toLowerCase(DANISH);
            // URL/Stream
            String info = "http://dr.dk/tv/api/live/info/" + uc + "/json";
            String strm = tmp.get(uc);
            if (strm == null || strm.isEmpty()) {
                LOGw(TAG, "Didn't get stream for " + uc);
                continue;
            }
            //String img = "file:///android_asset/" + uc + ".png";
            String img = "http://hehi00.nbi.dk:8888/~cholm/castdr/receiver/img/" + uc + ".png";
            LiveChannel sub = new LiveChannel(mLive, c, info, img, strm);
            mLive.addSub(sub);
        }
    }

    /**
     * Set-up newest
     */
    protected void setupNewest() {
        String url = queryUrl(true, "", "", "", false, true, MAX_NEWEST, 0);
        SubCategory sub = new SubCategory(mNewest, "", url);
        mNewest.addSub(sub);
    }

    /**
     * Set-up Genre category
     */
    protected void setupGenres() {
        LOGd(TAG, "Set-up genres category");
        // Get list of genres
        JSONArray array = null;
        if ((array = getJSONArray(API_MOBIL_URL + "genres")) == null) return;

        // Loop over genres
        for (int i = 0; i < array.length(); i++) {
            JSONObject obj = array.optJSONObject(i);
            if (obj == null) continue;

            String name = obj.optString("name");
            if (name == null || name.isEmpty()) continue;

            LOGd(TAG, " Adding genre " + name);
            String url =
                    queryUrl(true, "", name, "", false, true, MAX_GENRE, 0);
            SubCategory sub = new SubCategory(mGenres, name, url);
            mGenres.addSub(sub);
        }
        // Add fall-back
        SubCategory sub = new SubCategory(mGenres, "Ingen", "");
        mGenres.addSub(sub);
    }

    /**
     * Set-up Channels category
     */
    protected void setupChannels() {
        LOGd(TAG, "Set-up channels category");
        for (String c : CHANNELS) {
            String uc = c.toLowerCase(DANISH);
            String url =
                    queryUrl(true, "", "", uc, false, true, MAX_CHANNEL, 0);
            SubCategory sub = new SubCategory(mChannels, c, url);
            mChannels.addSub(sub);
        }
    }

    /**
     * Set-up letters category
     */
    protected void setupLetters() {
        LOGd(TAG, "Set-up Letters category");
        String l = "-ABCDEFGHIJKLMNOPQRSTUVWYZÆØÅ";
        for (char c : l.toCharArray()) {
            String sc = Character.toString(c);
            String url = queryUrl(true, sc, "", "", false, true, MAX_LETTER, 0);

            /* String tmp = url;
            switch (c) {
                case 'Æ': url = tmp.replace("title="+'Æ',"title=%U00C6"); break;
                case 'Ø': url = tmp.replace("title="+'Ø',"title=%U00D8"); break;
                case 'Å': url = tmp.replace("title="+'Å',"title=%U00C5"); break;
            }*/

            LOGd(TAG, "Query for letter " + c + ": " + url);
            SubCategory sub = new SubCategory(mLetters, sc, url);
            mLetters.addSub(sub);
        }
    }

    /**
     * Set-up newest
     */
    protected void setupSearch() {
        LOGd(TAG, "Set-up search category");
        Search search = new Search(mSearch, mGenres);
        mSearch.addSub(search);
    }

    /**
     * Load a sub-category
     *
     * @param sub
     * @param alsoFill
     */
    protected boolean loadSubCategory(SubCategory sub, boolean alsoFill) {
        return loadSubCategory(sub.getUrl(), alsoFill ? sub : null);
    }

    /**
     * Method to fill sub-category
     *
     * @param sub Sub-category
     * @return true on success
     */
    protected boolean loadSubCategory(SubCategory sub) {
        return loadSubCategory(sub.getUrl(), null);
    }

    /**
     * Method to fill series from a query into a list
     *
     * @param query Query
     * @param sub   List to fill
     * @return true on success
     */
    private boolean loadSubCategory(String query, SubCategory sub) {
        // Do the query
        JSONObject object = null;
        LOGd(TAG, "Loading sub-category from " + query);
        if ((object = getJSONObject(query)) == null) return false;

        // Get the programs
        JSONArray l = object.optJSONArray("ProgramSeries");
        return loadSubCategory(l, sub);
    }

    /**
     * Method to fill series from a query into a list
     *
     * @param l   JSON array
     * @param sub List to fill
     * @return true on success
     */
    private boolean loadSubCategory(JSONArray l, SubCategory sub) {
        if (l == null) {
            LOGe(TAG, "Didn't get the list of series");
            return false;
        }

        // Loop over programs
        for (int i = 0; i < l.length(); i++) {
            // Get the object
            JSONObject o = l.optJSONObject(i);
            if (o == null) {
                LOGe(TAG, "Didn't get serie at " + i);
                continue;
            }

            // Find the slug
            String slug = o.optString("ProgramSeriesSlug");
            if (slug == null || slug.isEmpty()) {
                slug = o.optString("programSerieSlug");
                if (slug == null || slug.isEmpty()) {
                    LOGe(TAG, "Didn't get slug of serie: " + o.toString());
                    continue;
                }
            }

            // See if we have the series in the cache already
            Serie s = mSeries.get(slug);
            if (!(s == null)) {
                if (sub != null) sub.addSerie(s);
                continue;
            }

            // Create new series object
            s = new Serie(o);
            if (!s.isValid()) {
                LOGe(TAG, "Serie " + slug + " invalid: " + o.toString());
                continue;
            }

            // Add it to our list and cache
            if (sub != null) sub.addSerie(s);
            mSeries.put(slug, s);

            // Register with genre
            mGenres.addSerie(s.getGenre(), s);

            // Register with letter
            mLetters.addSerie(s.getLetter(), s);

            // Register with channel
            mChannels.addSerie(s.getChannel(), s);
        }

        return true;
    }

    /**
     * Load the programs of a serie
     *
     * @param s Serie
     * @return true on success, false otherwise
     */
    protected boolean loadSerie(Serie s) {
        return loadSerie(s, true);
    }

    /**
     * Fill in program information
     *
     * @param s         Serie
     * @param alsoLinks If true, also find links
     * @return true on success
     */
    protected boolean loadSerie(Serie s, boolean alsoLinks) {
        if (s.getPrograms().size() > 0) {
            LOGw(TAG, "Serie " + s.getSlug() + " already loaded");
            return true;
        }

        JSONObject object = null;
        LOGd(TAG, "Loading program information from " + API_MOBIL_URL
                + "programserie?slug=" + s.getSlug());
        if ((object =
                getJSONObject(API_MOBIL_URL + "programserie?slug=" + s.getSlug())) == null)
            return false;

        String tit = object.optString("programSerieTitle");
        if (tit != null && !tit.isEmpty()) s.mTitle = tit;

        JSONArray programs = object.optJSONArray("videos");
        for (int i = 0; i < programs.length(); i++) {
            JSONObject o = programs.optJSONObject(i);
            if (o == null) {
                LOGe(TAG,
                        "No JSON object at " + i + ": " + programs.toString());
                continue;
            }
            Program v = new Program(o);
            v.mSerie = s;
            if (alsoLinks) {
                if (!loadProgram(v)) {
                    LOGe(TAG, "No links for " + v.getId());
                    continue;
                }
            }
            if (!v.isValid(alsoLinks)) {
                LOGe(TAG, "Invalid program @ " + i + ": " + o.toString());
                continue;
            }
            s.mPrograms.add(v);
        }
        return true;
    }

    /**
     * Fill in link information
     *
     * @param p Program to process
     * @return true on success
     */
    protected boolean loadProgram(Program p) {
        // If we already have links, do nothing
        if (p.hasLink()) return true;

        // Run the query
        JSONObject object = null;
        if ((object =
                getJSONObject(API_MOBIL_URL + "videos/" + p.getId()
                        + "?platform=android")) == null) return false;

        JSONObject streams = object.optJSONObject("streams");
        if (streams == null) {
            LOGe(TAG, "Didn't get list of streams");
            return false;
        }
        p.setLinks(streams);

        return p.hasLink();
    }

    /**
     * Utility to get a JSON object from a URL
     *
     * @param url Source URL
     * @return Retrieved object or null
     */
    protected JSONObject getJSONObject(String url) {
        RestClient.Result r = mRest.get(url);
        if (r == null || r.mObject == null) {
            LOGe(TAG, "Failed to get JSON object: " + url);
            return null;
        }
        return r.mObject;
    }

    /**
     * Utility to get an array JSON objects from a URL
     *
     * @param url Source URL
     * @return Retrieved object or null
     */
    protected JSONArray getJSONArray(String url) {
        RestClient.Result r = mRest.get(url);
        if (r.mArray == null) {
            LOGe(TAG, "Failed to get JSON object: " + url);
            return null;
        }
        return r.mArray;
    }

    /**
     * Get an option string
     *
     * @param key   Key
     * @param value Value
     * @return Query parameter
     */
    protected String getQueryParam(String key, String value) {
        return ("&" + key + "=" + (value == null ? "" : value));
        /*
        try {
            return ("&" + key + "=" + URLEncoder.encode((value == null) ? "" : value, "ISO-8859-1"));
        } catch (UnsupportedEncodingException e) {
            LOGw(TAG, "Failed to encode " + key + "=" + value, e);
        }
        return "&" + key + "=";
        */
    }

    /**
     * Form a query URL
     *
     * @param title    Title selection
     * @param genre    Genre selection
     * @param channel  Channel selection
     * @param previews Whether to include previews
     * @param byDate   Whether to sort by date
     * @param limit    Maximum number of results to retrieve
     * @param offset   Off-set in result list
     * @return The query URL
     */
    protected String queryUrl(boolean start, String title, String genre,
                              String channel, boolean previews, boolean byDate,
                              int limit, int offset) {
        // date=true -> OK
        // date=true + channel -> OK
        // date=true + genre -> OK
        // date=true + title -> OK (title ignored)
        // date=true + channel + title -> OK
        // date=true + channel + genre -> OK
        // date=true + genre + title -> OK (title ignored)
        // date=true + channel + genre + title -> OK (channel+title ignored)
        // date=false -> empty
        // date=false + channel -> empty
        // date=false + genre -> empty
        // date=false + title -> OK
        // date=false + channel + genre -> empty
        // date=false + channel + title -> OK
        // date=false + genre + title -> OK (genre ignored)
        // date=false + channel + genre + title -> OK (genre ignored)

        // String ret = "http://www.dr.dk/tv/api/programmap?";
        String ret = "searchType=" + (start ? "startswith" : "contains");
        ret += getQueryParam("title", title.replaceAll(" ", "%20"));
        ret += getQueryParam("genre", genre.replaceAll(" ", "%20"));
        ret += getQueryParam("channelSlug", channel);
        ret += getQueryParam("includePreviews", Boolean.toString(previews));
        ret += getQueryParam("orderByDate", Boolean.toString(byDate));
        ret += getQueryParam("limit", Integer.toString(limit));
        ret += getQueryParam("offset", Integer.toString(offset));

        String uri = "";
        try {
            uri =
                    URIUtils.createURI("http",
                            "www.dr.dk",
                            0,
                            "tv/api/programmap",
                            ret,
                            "").toString();
            if (uri.endsWith("#")) uri = uri.substring(0, uri.length() - 1);
        } catch (URISyntaxException e) {
            LOGe(TAG, e.getMessage());
        }
        // LOGd(TAG, "Query URI: " + uri);
        return uri;
    }

    /**
     * Get the genre category
     *
     * @return Genre category
     */
    public Category getLIVE() {
        return mLive;
    }

    /**
     * Get the newest category
     *
     * @return Newest
     */
    public Category getNewest() {
        return mNewest;
    }

    /**
     * Get the genre category
     *
     * @return Genre category
     */
    public Category getGenres() {
        return mGenres;
    }

    /**
     * Get the channels category
     *
     * @return Channel category
     */
    public Category getChannels() {
        return mChannels;
    }

    /**
     * Get the letters category
     *
     * @return Letters
     */
    public Category getLetters() {
        return mLetters;
    }

    /**
     * Get the newest category
     *
     * @return Newest
     */
    public Category getSearch() {
        return mSearch;
    }

}
// EOF
